package com.sudoku.board;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Stack;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.sudoku.command.Command;
import com.sudoku.mode.Mode;
import com.sudoku.mode.Ordinary;
import com.sudoku.observer.Event;
import com.sudoku.observer.Observer;
import com.sudoku.observer.Subject;
import com.sudoku.utility.Constants;
import com.sudoku.utility.Locator;

public class GameHandler implements Constants, Observer {

    private Mode gameMode;
    private Tile tileHovered;
    private Tile tileSelected;
    private TableButtons buttons;
    private boolean pencilMode;
    private Stack<Command> commands;
    private float gameTimer;
    private boolean calcTime;
    private Debug debug;
    private boolean solvedBoard;
    private boolean allFilled;
    private Subject subject;
    private boolean showSolvedBanner;
    private float bannerTimer;
    private ShapeRenderer banner;
    private BitmapFont smallFont;

    public static boolean DEBUG = false;

    public GameHandler(Subject subject, int mode) {
        tileHovered = null;
        tileSelected = null;
        pencilMode = false;
        calcTime = false;
        allFilled = false;
        solvedBoard = false;
        gameTimer = -1;
        this.subject = subject;
        banner = new ShapeRenderer();
        subject.addObserver(this);
        buttons = new TableButtons(this, subject);
        commands = new Stack<Command>();
        
        // fonts
        FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal(FONT));
        FreeTypeFontParameter param = new FreeTypeFontParameter();
        param.size = (int) (80);
        smallFont = generator.generateFont(param);
        smallFont.setColor(Color.WHITE);
        generator.dispose();

        switch (mode) {
            case ORDINARY:
                gameMode = new Ordinary(subject);
                break;
            default:
                gameMode = new Ordinary(subject);
                break;
        }

        banner.setTransformMatrix(Locator.getCamera().view);
        banner.setProjectionMatrix(Locator.getCamera().projection);

    }

    public void startGame() {
        reset();
        Locator.gameInProgress = true;
        gameMode.startGame();
        debug = new Debug(gameMode.getBoard());
        if (DEBUG) {
            debug.pencilAllTiles();
        }
    }

    public void reset() {
        showSolvedBanner = false;
        calcTime = false;
        solvedBoard = false;
        allFilled = false;
        commands.clear();
        gameMode.reset();
        bannerTimer = 0;
        gameTimer = -1;

    }

    public void render(SpriteBatch sb, BitmapFont font, BitmapFont pencilFont) {
        gameMode.render(sb, font, pencilFont);
        buttons.render(sb);

        //Changed in the options menu
        if (Locator.showTimer) {
            renderTimer(sb, font);
        }
        //Changed in the options menu
        if (Locator.showHints) {
            renderHints(sb, font);
        }

        if (allFilled && !solvedBoard) {
            renderWrong(sb, font);
        }

        if (showSolvedBanner) {
            bannerTimer += Gdx.graphics.getDeltaTime();
            renderCongrats(sb);
            if (bannerTimer > 6) {
                showSolvedBanner = false;
                bannerTimer = 0;
            }
        }
    }

    private void renderCongrats(SpriteBatch sb) {
        Gdx.gl.glEnable(GL20.GL_BLEND);
        Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
        banner.begin(ShapeType.Filled);
        banner.setColor(0, 0, 0, (bannerTimer) / 3);
        banner.rect(0, ACTUAL_VIRTUAL_HEIGHT * 0.4F, ACTUAL_VIRTUAL_WIDTH * 100, ACTUAL_VIRTUAL_HEIGHT * 0.2F);
        banner.end();
        Gdx.gl.glDisable(GL20.GL_BLEND);

        sb.begin();
        smallFont.draw(sb, "Puzzle Solved!", ACTUAL_VIRTUAL_WIDTH * 0.34f, ACTUAL_VIRTUAL_HEIGHT * 0.52f);
        sb.end();
    }

    private void renderWrong(SpriteBatch sb, BitmapFont font) {
        font.setColor(Color.RED);
        sb.begin();
        font.draw(sb, "Wrong", ACTUAL_VIRTUAL_WIDTH * 0.16f, ACTUAL_VIRTUAL_HEIGHT * 0.98f);
        sb.end();
        font.setColor(Color.BLACK);
    }

    private void renderHints(SpriteBatch sb, BitmapFont font) {
        sb.begin();
        font.draw(sb, "Hints Used: " + gameMode.getHintsUsed(), ACTUAL_VIRTUAL_WIDTH * 0.205f, ACTUAL_VIRTUAL_HEIGHT * 0.15f);
        sb.end();
    }

    private void renderTimer(SpriteBatch sb, BitmapFont font) {
        sb.begin();
        String min = "00";
        min = min.concat(String.valueOf((int) (gameTimer / 60)));
        String sec = "00";
        sec = sec.concat(String.valueOf((int) (gameTimer % 60)));
        font.draw(sb, min.substring(min.length() - 2, min.length()) + ":" + sec.substring(sec.length() - 2, sec.length()),
                ACTUAL_VIRTUAL_WIDTH * 0.33f, ACTUAL_VIRTUAL_HEIGHT * 0.06f);
        sb.end();
    }

    public void update(float delta) {
        gameMode.update(delta);
        buttons.update(delta);
        if (calcTime) {
            gameTimer = 0;
            calcTime = false;
        } else if (!solvedBoard) {
            gameTimer += delta;
        }

    }

    public void changeTileColor(float x, float y) {
        gameMode.unselectTile(tileSelected);
        gameMode.unhover(tileHovered);
        // invert y value
        float tempY = Math.max(Locator.getViewport().getViewportHeight() - y + Locator.yOffset, 0);
        for (Box b : gameMode.getBoard().getData()) {
            // check if position in within that box
            if (x >= b.getXViewPosition() && tempY >= b.getYViewPosition() && x < b.getXViewPosition() + Locator.BOX_PIXEL_SIZE
                    && tempY < b.getYViewPosition() + Locator.BOX_PIXEL_SIZE) {

                for (Tile t : b.getData()) {
                    // check if position is within the tile
                    if (t.containsPosition(x, tempY)) {
                        if (tileHovered != null && !tileHovered.equals(t)) {
                            subject.notify(Event.EVENT_SELECT_TILE);
                        }
                        tileHovered = t;
                        gameMode.hover(tileHovered);
                    }
                }
            }
        }
    }

    public void selectTile() {
        if (tileHovered != null) {
            subject.notify(Event.EVENT_SELECT_TILE);
            gameMode.unselectTile(tileSelected);
            tileSelected = tileHovered;
            gameMode.setSelectedTile(tileSelected);
            tileHovered = null;
        }
    }

    public void setTileData(char character) {
        Command command = null;
        if (!pencilMode) {
            command = gameMode.setTileData(character);
        } else {
            command = gameMode.setPencilTileData(character);
        }
        addAndExecuteCommand(command);
        if (command != null) {
            updateDataSave(gameMode.getSelectedTile());
        }
    }

    @Override
    public void onNotify(Event event) {
        switch (event) {
            case EVENT_HINT:
                //Changed in the options menu
                if (Locator.showHints) {
                    gameMode.hint();
                }
                break;
            case EVENT_NUMBER_SELECTED:
                setTileData(buttons.getNumberSelected());
                //Check if player has won
                if (checkForWin()) {
                    showSolvedBanner = true;
                    subject.notify(Event.EVENT_WIN_PUZZLE);
                }
                if (DEBUG) {
                    debug.pencilAllTiles();
                }
                break;

            case EVENT_PENCIL:
                pencilMode = !pencilMode;
                gameMode.switchPencilMode();
                break;
            case EVENT_ERASE:
                erase();
                break;
            case EVENT_SOLVE:
                gameMode.solve();
                break;
            case EVENT_CHECK_DONE:
                gameMode.rehighlight();
                break;
            case EVENT_SOLVER_DONE:
                calcTime = true;
                Locator.writeSave();
                break;
            case EVENT_SOLVED:
                gameMode.solverDone();
                break;
            default:
                break;
        }

    }

    private boolean checkForWin() {
        solvedBoard = true;
        allFilled = true;
        for (int i = 0; i < 81; i++) {
            if (gameMode.getBoard().getDataFromIndex(i).getData() == ' ') {
                solvedBoard = false;
                allFilled = false;
                break;
            }
        }

        HashSet<Integer> base = new HashSet<Integer>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
        // check for duplicates in rows
        for (int i = 0; i < 9 && solvedBoard; i++) {
            base.removeAll(gameMode.getBoard().getBoardRow(i));
            if (base.size() != 0) {
                solvedBoard = false;
                break;
            }
            base.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
        }
        // check for duplicates in cols
        for (int i = 0; i < 9 && solvedBoard; i++) {
            base.removeAll(gameMode.getBoard().getBoardCol(i));
            if (base.size() != 0) {
                solvedBoard = false;
                break;
            }
            base.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
        }

        // check for duplicates in cols
        for (int i = 0; i < 9 && solvedBoard; i++) {
            base.removeAll(gameMode.getBoard().getData().get(i).getNumbers());
            if (base.size() != 0) {
                solvedBoard = false;
                break;
            }
            base.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
        }
        
        if (allFilled && !solvedBoard) {
            subject.notify(Event.EVENT_LOSE_PUZZLE);
        }

        return solvedBoard;
    }

    public void erase() {
        Command command = gameMode.erase();
        addAndExecuteCommand(command);
    }

    public void undo() {
        // remove and undo the last command that was executed
        if (!commands.isEmpty()) {
            commands.pop().undo();
        }
        allFilled = false;
    }

    public void addAndExecuteCommand(Command command) {
        if (command != null) {
            commands.push(command);
            command.execute();
            // only store up to 64 undos
            if (commands.size() > 64) {
                commands.remove(0);
            }
        }

    }

    public void debug() {
        gameMode.debug();

    }

    /**
     * Save the new tile data
     * @param tile
     */
    public void updateDataSave(Tile tile) {
        Preferences prefs = Gdx.app.getPreferences("My Preferences");
        int index = (int) (tile.getBox().getCoords().x * COLS_PER_BOX + tile.getCoords().x + 9 * (tile.getBox().getCoords().y * ROWS_PER_BOX + tile
                .getCoords().y));
        prefs.putString("data" + index, Character.toString(tile.getData()));
        //write the pencil numbers
        int j = 0;
        for (; j < tile.getPencilNumbers().size(); j++) {
            prefs.putInteger("pencilData" + index + j, tile.getPencilNumbers().get(j));
        }
        prefs.putInteger("pencilDataSize" + index, j);
        prefs.flush();
    }

    /**
     * Save the data of the whole board
     * @param prefs
     */
    public void writeSave(Preferences prefs) {
        Board board = gameMode.getBoard();

        for (int i = 0; i < 81; i++) {
            Tile tile = board.getDataFromIndex(i);
            //write the tile data
            prefs.putString("data" + i, Character.toString(tile.getData()));
            //write permanent data
            prefs.putBoolean("perm" + i, tile.isPermanent());
            if (tile.getData() == ' ') {
                //write the pencil numbers
                int j = 0;
                for (; j < tile.getPencilNumbers().size(); j++) {
                    prefs.putInteger("pencilData" + i + j, tile.getPencilNumbers().get(j));
                }
                prefs.putInteger("pencilDataSize" + i, j);
            }
        }

        //save the time
        prefs.putFloat("timer", gameTimer);
        gameMode.writeSave(prefs);
    }

    /**
     * Read the save data for the board
     * @param prefs
     */
    public void readSave(Preferences prefs) {
        Board board = gameMode.getBoard();

        for (int i = 0; i < 81; i++) {
            Tile tile = board.getDataFromIndex(i);
            //read the tile data
            tile.setData(prefs.getString("data" + i, "0").charAt(0));

            //read permanent data
            if (prefs.getBoolean("perm" + i, true)) {
                tile.setPermanent(true);
            }
            if (tile.getData() == ' ') {
                //read the pencil numbers
                int size = prefs.getInteger("pencilDataSize" + i, 0);
                for (int j = 0; j < size; j++) {
                    tile.getPencilNumbers().add(prefs.getInteger("pencilData" + i + j, 0));
                }
            }
        }
        gameTimer = prefs.getFloat("timer", 0);
        gameMode.readSave(prefs);

        debug = new Debug(gameMode.getBoard());
    }

}
